<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <title>实现一个2048游戏</title>
    <style>
        html,
        body {
            margin: 0;
            padding: 0;
            background: #faf8ef;
            color: #776e65;
            font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif;
            font-size: 18px;
        }

        body {
            margin: 20px 0;
        }

        .heading:after {
            content: "";
            display: block;
            clear: both;
        }

        h1.title {
            font-size: 80px;
            font-weight: bold;
            margin: 0;
            display: block;
            float: left;
        }

        @-webkit-keyframes move-up {
            0% {
                top: 25px;
                opacity: 1;
            }

            100% {
                top: -50px;
                opacity: 0;
            }
        }

        @-moz-keyframes move-up {
            0% {
                top: 25px;
                opacity: 1;
            }

            100% {
                top: -50px;
                opacity: 0;
            }
        }

        @keyframes move-up {
            0% {
                top: 25px;
                opacity: 1;
            }

            100% {
                top: -50px;
                opacity: 0;
            }
        }

        .score-container {
            position: relative;
            float: right;
            background: #bbada0;
            padding: 15px 25px;
            font-size: 25px;
            height: 25px;
            line-height: 47px;
            font-weight: bold;
            border-radius: 3px;
            color: white;
            margin-top: 8px;
        }

        .score-container:after {
            position: absolute;
            width: 100%;
            top: 10px;
            left: 0;
            content: "Score";
            text-transform: uppercase;
            font-size: 13px;
            line-height: 13px;
            text-align: center;
            color: #eee4da;
        }

        .score-container .score-addition {
            position: absolute;
            right: 30px;
            color: red;
            font-size: 25px;
            line-height: 25px;
            font-weight: bold;
            color: rgba(119, 110, 101, 0.9);
            z-index: 100;
            -webkit-animation: move-up 600ms ease-in;
            -moz-animation: move-up 600ms ease-in;
            -webkit-animation-fill-mode: both;
            -moz-animation-fill-mode: both;
        }

        p {
            margin-top: 0;
            margin-bottom: 10px;
            line-height: 1.65;
        }

        a {
            color: #776e65;
            font-weight: bold;
            text-decoration: underline;
            cursor: pointer;
        }

        strong.important {
            text-transform: uppercase;
        }

        hr {
            border: none;
            border-bottom: 1px solid #d8d4d0;
            margin-top: 20px;
            margin-bottom: 30px;
        }

        .container {
            width: 500px;
            margin: 0 auto;
        }

        @-webkit-keyframes fade-in {
            0% {
                opacity: 0;
            }

            100% {
                opacity: 1;
            }
        }

        @-moz-keyframes fade-in {
            0% {
                opacity: 0;
            }

            100% {
                opacity: 1;
            }
        }

        @keyframes fade-in {
            0% {
                opacity: 0;
            }

            100% {
                opacity: 1;
            }
        }

        .game-container {
            margin-top: 10px;
            position: relative;
            padding: 15px;
            cursor: default;
            -webkit-touch-callout: none;
            -webkit-user-select: none;
            -moz-user-select: none;
            background: #bbada0;
            border-radius: 6px;
            width: 500px;
            height: 500px;
            -webkit-box-sizing: border-box;
            -moz-box-sizing: border-box;
            box-sizing: border-box;
        }

        .game-container .game-message {
            display: none;
            position: absolute;
            top: 0;
            right: 0;
            bottom: 0;
            left: 0;
            background: rgba(238, 228, 218, 0.5);
            z-index: 100;
            text-align: center;
            -webkit-animation: fade-in 800ms ease 1200ms;
            -moz-animation: fade-in 800ms ease 1200ms;
            -webkit-animation-fill-mode: both;
            -moz-animation-fill-mode: both;
        }

        .game-container .game-message p {
            font-size: 60px;
            font-weight: bold;
            height: 60px;
            line-height: 60px;
            margin-top: 222px;
        }

        .game-container .game-message .lower {
            display: block;
            margin-top: 59px;
        }

        .game-container .game-message a {
            display: inline-block;
            background: #8f7a66;
            border-radius: 3px;
            padding: 0 20px;
            text-decoration: none;
            color: #f9f6f2;
            height: 40px;
            line-height: 42px;
            margin-left: 9px;
        }

        .game-container .game-message.game-won {
            background: rgba(237, 194, 46, 0.5);
            color: #f9f6f2;
        }

        .game-container .game-message.game-won,
        .game-container .game-message.game-over {
            display: block;
        }

        .grid-container {
            position: absolute;
            z-index: 1;
        }

        .grid-row {
            margin-bottom: 15px;
        }

        .grid-row:last-child {
            margin-bottom: 0;
        }

        .grid-row:after {
            content: "";
            display: block;
            clear: both;
        }

        .grid-cell {
            width: 106.25px;
            height: 106.25px;
            margin-right: 15px;
            float: left;
            border-radius: 3px;
            background: rgba(238, 228, 218, 0.35);
        }

        .grid-cell:last-child {
            margin-right: 0;
        }

        .tile-container {
            position: absolute;
            z-index: 2;
        }

        .tile {
            width: 106.25px;
            height: 106.25px;
            line-height: 116.25px;
        }

        .tile.tile-position-1-1 {
            position: absolute;
            left: 0px;
            top: 0px;
        }

        .tile.tile-position-1-2 {
            position: absolute;
            left: 0px;
            top: 121px;
        }

        .tile.tile-position-1-3 {
            position: absolute;
            left: 0px;
            top: 243px;
        }

        .tile.tile-position-1-4 {
            position: absolute;
            left: 0px;
            top: 364px;
        }

        .tile.tile-position-2-1 {
            position: absolute;
            left: 121px;
            top: 0px;
        }

        .tile.tile-position-2-2 {
            position: absolute;
            left: 121px;
            top: 121px;
        }

        .tile.tile-position-2-3 {
            position: absolute;
            left: 121px;
            top: 243px;
        }

        .tile.tile-position-2-4 {
            position: absolute;
            left: 121px;
            top: 364px;
        }

        .tile.tile-position-3-1 {
            position: absolute;
            left: 243px;
            top: 0px;
        }

        .tile.tile-position-3-2 {
            position: absolute;
            left: 243px;
            top: 121px;
        }

        .tile.tile-position-3-3 {
            position: absolute;
            left: 243px;
            top: 243px;
        }

        .tile.tile-position-3-4 {
            position: absolute;
            left: 243px;
            top: 364px;
        }

        .tile.tile-position-4-1 {
            position: absolute;
            left: 364px;
            top: 0px;
        }

        .tile.tile-position-4-2 {
            position: absolute;
            left: 364px;
            top: 121px;
        }

        .tile.tile-position-4-3 {
            position: absolute;
            left: 364px;
            top: 243px;
        }

        .tile.tile-position-4-4 {
            position: absolute;
            left: 364px;
            top: 364px;
        }

        .tile {
            border-radius: 3px;
            background: #eee4da;
            text-align: center;
            font-weight: bold;
            z-index: 10;
            font-size: 55px;
            -webkit-transition: 100ms ease-in-out;
            -moz-transition: 100ms ease-in-out;
            -webkit-transition-property: top, left;
            -moz-transition-property: top, left;
        }

        .tile.tile-2 {
            background: #eee4da;
            box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0);
        }

        .tile.tile-4 {
            background: #ede0c8;
            box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0);
        }

        .tile.tile-8 {
            color: #f9f6f2;
            background: #f2b179;
        }

        .tile.tile-16 {
            color: #f9f6f2;
            background: #f59563;
        }

        .tile.tile-32 {
            color: #f9f6f2;
            background: #f67c5f;
        }

        .tile.tile-64 {
            color: #f9f6f2;
            background: #f65e3b;
        }

        .tile.tile-128 {
            color: #f9f6f2;
            background: #edcf72;
            box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.2381), inset 0 0 0 1px rgba(255, 255, 255, 0.14286);
            font-size: 45px;
        }

        @media screen and (max-width:480px) {
            .tile.tile-128 {
                font-size: 25px;
            }
        }

        .tile.tile-256 {
            color: #f9f6f2;
            background: #edcc61;
            box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.31746), inset 0 0 0 1px rgba(255, 255, 255, 0.19048);
            font-size: 45px;
        }

        @media screen and (max-width:480px) {
            .tile.tile-256 {
                font-size: 25px;
            }
        }

        .tile.tile-512 {
            color: #f9f6f2;
            background: #edc850;
            box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.39683), inset 0 0 0 1px rgba(255, 255, 255, 0.2381);
            font-size: 45px;
        }

        @media screen and (max-width:480px) {
            .tile.tile-512 {
                font-size: 25px;
            }
        }

        .tile.tile-1024 {
            color: #f9f6f2;
            background: #edc53f;
            box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.47619), inset 0 0 0 1px rgba(255, 255, 255, 0.28571);
            font-size: 35px;
        }

        @media screen and (max-width:480px) {
            .tile.tile-1024 {
                font-size: 15px;
            }
        }

        .tile.tile-2048 {
            color: #f9f6f2;
            background: #edc22e;
            box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.55556), inset 0 0 0 1px rgba(255, 255, 255, 0.33333);
            font-size: 35px;
        }

        @media screen and (max-width:480px) {
            .tile.tile-2048 {
                font-size: 15px;
            }
        }

        @-webkit-keyframes appear {
            0% {
                opacity: 0;
                -webkit-transform: scale(0);
                -moz-transform: scale(0);
            }

            100% {
                opacity: 1;
                -webkit-transform: scale(1);
                -moz-transform: scale(1);
            }
        }

        @-moz-keyframes appear {
            0% {
                opacity: 0;
                -webkit-transform: scale(0);
                -moz-transform: scale(0);
            }

            100% {
                opacity: 1;
                -webkit-transform: scale(1);
                -moz-transform: scale(1);
            }
        }

        @keyframes appear {
            0% {
                opacity: 0;
                -webkit-transform: scale(0);
                -moz-transform: scale(0);
            }

            100% {
                opacity: 1;
                -webkit-transform: scale(1);
                -moz-transform: scale(1);
            }
        }

        .tile-new {
            -webkit-animation: appear 200ms ease 100ms;
            -moz-animation: appear 200ms ease 100ms;
            -webkit-animation-fill-mode: both;
            -moz-animation-fill-mode: both;
        }

        @-webkit-keyframes pop {
            0% {
                -webkit-transform: scale(0);
                -moz-transform: scale(0);
            }

            50% {
                -webkit-transform: scale(1.2);
                -moz-transform: scale(1.2);
            }

            100% {
                -webkit-transform: scale(1);
                -moz-transform: scale(1);
            }
        }

        @-moz-keyframes pop {
            0% {
                -webkit-transform: scale(0);
                -moz-transform: scale(0);
            }

            50% {
                -webkit-transform: scale(1.2);
                -moz-transform: scale(1.2);
            }

            100% {
                -webkit-transform: scale(1);
                -moz-transform: scale(1);
            }
        }

        @keyframes pop {
            0% {
                -webkit-transform: scale(0);
                -moz-transform: scale(0);
            }

            50% {
                -webkit-transform: scale(1.2);
                -moz-transform: scale(1.2);
            }

            100% {
                -webkit-transform: scale(1);
                -moz-transform: scale(1);
            }
        }

        .tile-merged {
            z-index: 20;
            -webkit-animation: pop 200ms ease 100ms;
            -moz-animation: pop 200ms ease 100ms;
            -webkit-animation-fill-mode: both;
            -moz-animation-fill-mode: both;
        }

        .game-intro {
            margin-bottom: 0;
        }

        .game-explanation {
            margin-top: 50px;
        }

        @media screen and (max-width:480px) {

            html,
            body {
                font-size: 15px;
            }

            body {
                margin: 20px 0;
                padding: 0 20px;
            }

            h1.title {
                font-size: 50px;
            }

            .container {
                width: 280px;
                margin: 0 auto;
            }

            .score-container {
                margin-top: 0;
            }

            .heading {
                margin-bottom: 10px;
            }

            .game-container {
                margin-top: 40px;
                position: relative;
                padding: 10px;
                cursor: default;
                -webkit-touch-callout: none;
                -webkit-user-select: none;
                -moz-user-select: none;
                background: #bbada0;
                border-radius: 6px;
                width: 280px;
                height: 280px;
                -webkit-box-sizing: border-box;
                -moz-box-sizing: border-box;
                box-sizing: border-box;
            }

            .game-container .game-message {
                display: none;
                position: absolute;
                top: 0;
                right: 0;
                bottom: 0;
                left: 0;
                background: rgba(238, 228, 218, 0.5);
                z-index: 100;
                text-align: center;
                -webkit-animation: fade-in 800ms ease 1200ms;
                -moz-animation: fade-in 800ms ease 1200ms;
                -webkit-animation-fill-mode: both;
                -moz-animation-fill-mode: both;
            }

            .game-container .game-message p {
                font-size: 60px;
                font-weight: bold;
                height: 60px;
                line-height: 60px;
                margin-top: 222px;
            }

            .game-container .game-message .lower {
                display: block;
                margin-top: 59px;
            }

            .game-container .game-message a {
                display: inline-block;
                background: #8f7a66;
                border-radius: 3px;
                padding: 0 20px;
                text-decoration: none;
                color: #f9f6f2;
                height: 40px;
                line-height: 42px;
                margin-left: 9px;
            }

            .game-container .game-message.game-won {
                background: rgba(237, 194, 46, 0.5);
                color: #f9f6f2;
            }

            .game-container .game-message.game-won,
            .game-container .game-message.game-over {
                display: block;
            }

            .grid-container {
                position: absolute;
                z-index: 1;
            }

            .grid-row {
                margin-bottom: 10px;
            }

            .grid-row:last-child {
                margin-bottom: 0;
            }

            .grid-row:after {
                content: "";
                display: block;
                clear: both;
            }

            .grid-cell {
                width: 57.5px;
                height: 57.5px;
                margin-right: 10px;
                float: left;
                border-radius: 3px;
                background: rgba(238, 228, 218, 0.35);
            }

            .grid-cell:last-child {
                margin-right: 0;
            }

            .tile-container {
                position: absolute;
                z-index: 2;
            }

            .tile {
                width: 57.5px;
                height: 57.5px;
                line-height: 67.5px;
            }

            .tile.tile-position-1-1 {
                position: absolute;
                left: 0px;
                top: 0px;
            }

            .tile.tile-position-1-2 {
                position: absolute;
                left: 0px;
                top: 68px;
            }

            .tile.tile-position-1-3 {
                position: absolute;
                left: 0px;
                top: 135px;
            }

            .tile.tile-position-1-4 {
                position: absolute;
                left: 0px;
                top: 203px;
            }

            .tile.tile-position-2-1 {
                position: absolute;
                left: 68px;
                top: 0px;
            }

            .tile.tile-position-2-2 {
                position: absolute;
                left: 68px;
                top: 68px;
            }

            .tile.tile-position-2-3 {
                position: absolute;
                left: 68px;
                top: 135px;
            }

            .tile.tile-position-2-4 {
                position: absolute;
                left: 68px;
                top: 203px;
            }

            .tile.tile-position-3-1 {
                position: absolute;
                left: 135px;
                top: 0px;
            }

            .tile.tile-position-3-2 {
                position: absolute;
                left: 135px;
                top: 68px;
            }

            .tile.tile-position-3-3 {
                position: absolute;
                left: 135px;
                top: 135px;
            }

            .tile.tile-position-3-4 {
                position: absolute;
                left: 135px;
                top: 203px;
            }

            .tile.tile-position-4-1 {
                position: absolute;
                left: 203px;
                top: 0px;
            }

            .tile.tile-position-4-2 {
                position: absolute;
                left: 203px;
                top: 68px;
            }

            .tile.tile-position-4-3 {
                position: absolute;
                left: 203px;
                top: 135px;
            }

            .tile.tile-position-4-4 {
                position: absolute;
                left: 203px;
                top: 203px;
            }

            .game-container {
                margin-top: 20px;
            }

            .tile {
                font-size: 35px;
            }

            .game-message p {
                font-size: 30px !important;
                height: 30px !important;
                line-height: 30px !important;
                margin-top: 90px !important;
            }

            .game-message .lower {
                margin-top: 30px !important;
            }
    </style>
</head>

<body>
    <div class="container">
        <div class="heading">
            <h1 class="title">2048</h1>
            <div class="score-container">0</div>
        </div>
        <div class="game-container">
            <div class="game-message">
                <p></p>
                <div class="lower"><a class="retry-button">重玩</a></div>
            </div>
            <div class="grid-container">
                <div class="grid-row">
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                </div>
                <div class="grid-row">
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                </div>
                <div class="grid-row">
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                </div>
                <div class="grid-row">
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                    <div class="grid-cell"></div>
                </div>
            </div>
            <div class="tile-container"></div>
        </div>
        <p class="game-explanation"><strong class="important">提示:</strong>按方向键或者触摸屏幕即可开玩</p>
        <hr />
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/1.0.6/hammer.min.js"></script>
    <script>document.addEventListener('DOMContentLoaded', function () { window.requestAnimationFrame(function () { var manager = new GameManager(4, KeyboardInputManager, HTMLActuator) }) }); function GameManager(size, InputManager, Actuator) { this.size = size; this.inputManager = new InputManager(); this.actuator = new Actuator(); this.startTiles = 2; this.inputManager.on('move', this.move.bind(this)); this.inputManager.on('restart', this.restart.bind(this)); this.setup() } GameManager.prototype.restart = function () { this.actuator.restart(); this.setup() }; GameManager.prototype.setup = function () { this.grid = new Grid(this.size); this.score = 0; this.over = false; this.won = false; this.addStartTiles(); this.actuate() }; GameManager.prototype.addStartTiles = function () { for (var i = 0; i < this.startTiles; i++) { this.addRandomTile() } }; GameManager.prototype.addRandomTile = function () { if (this.grid.cellsAvailable()) { var value = Math.random() < 0.9 ? 2 : 4; var tile = new Tile(this.grid.randomAvailableCell(), value); this.grid.insertTile(tile) } }; GameManager.prototype.actuate = function () { this.actuator.actuate(this.grid, { score: this.score, over: this.over, won: this.won, }) }; GameManager.prototype.prepareTiles = function () { this.grid.eachCell(function (x, y, tile) { if (tile) { tile.mergedFrom = null; tile.savePosition() } }) }; GameManager.prototype.moveTile = function (tile, cell) { this.grid.cells[tile.x][tile.y] = null; this.grid.cells[cell.x][cell.y] = tile; tile.updatePosition(cell) }; GameManager.prototype.move = function (direction) { var self = this; if (this.over || this.won) return; var cell, tile; var vector = this.getVector(direction); var traversals = this.buildTraversals(vector); var moved = false; this.prepareTiles(); traversals.x.forEach(function (x) { traversals.y.forEach(function (y) { cell = { x: x, y: y }; tile = self.grid.cellContent(cell); if (tile) { var positions = self.findFarthestPosition(cell, vector); var next = self.grid.cellContent(positions.next); if (next && next.value === tile.value && !next.mergedFrom) { var merged = new Tile(positions.next, tile.value * 2); merged.mergedFrom = [tile, next]; self.grid.insertTile(merged); self.grid.removeTile(tile); tile.updatePosition(positions.next); self.score += merged.value; if (merged.value === 2048) self.won = true } else { self.moveTile(tile, positions.farthest) } if (!self.positionsEqual(cell, tile)) { moved = true } } }) }); if (moved) { this.addRandomTile(); if (!this.movesAvailable()) { this.over = true } this.actuate() } }; GameManager.prototype.getVector = function (direction) { var map = { 0: { x: 0, y: -1 }, 1: { x: 1, y: 0 }, 2: { x: 0, y: 1 }, 3: { x: -1, y: 0 }, }; return map[direction] }; GameManager.prototype.buildTraversals = function (vector) { var traversals = { x: [], y: [] }; for (var pos = 0; pos < this.size; pos++) { traversals.x.push(pos); traversals.y.push(pos) } if (vector.x === 1) traversals.x = traversals.x.reverse(); if (vector.y === 1) traversals.y = traversals.y.reverse(); return traversals }; GameManager.prototype.findFarthestPosition = function (cell, vector) { var previous; do { previous = cell; cell = { x: previous.x + vector.x, y: previous.y + vector.y } } while (this.grid.withinBounds(cell) && this.grid.cellAvailable(cell)); return { farthest: previous, next: cell, } }; GameManager.prototype.movesAvailable = function () { return this.grid.cellsAvailable() || this.tileMatchesAvailable() }; GameManager.prototype.tileMatchesAvailable = function () { var self = this; var tile; for (var x = 0; x < this.size; x++) { for (var y = 0; y < this.size; y++) { tile = this.grid.cellContent({ x: x, y: y }); if (tile) { for (var direction = 0; direction < 4; direction++) { var vector = self.getVector(direction); var cell = { x: x + vector.x, y: y + vector.y }; var other = self.grid.cellContent(cell); if (other) { } if (other && other.value === tile.value) { return true } } } } } return false }; GameManager.prototype.positionsEqual = function (first, second) { return first.x === second.x && first.y === second.y }; function Grid(size) { this.size = size; this.cells = []; this.build() } Grid.prototype.build = function () { for (var x = 0; x < this.size; x++) { var row = (this.cells[x] = []); for (var y = 0; y < this.size; y++) { row.push(null) } } }; Grid.prototype.randomAvailableCell = function () { var cells = this.availableCells(); if (cells.length) { return cells[Math.floor(Math.random() * cells.length)] } }; Grid.prototype.availableCells = function () { var cells = []; this.eachCell(function (x, y, tile) { if (!tile) { cells.push({ x: x, y: y }) } }); return cells }; Grid.prototype.eachCell = function (callback) { for (var x = 0; x < this.size; x++) { for (var y = 0; y < this.size; y++) { callback(x, y, this.cells[x][y]) } } }; Grid.prototype.cellsAvailable = function () { return !!this.availableCells().length }; Grid.prototype.cellAvailable = function (cell) { return !this.cellOccupied(cell) }; Grid.prototype.cellOccupied = function (cell) { return !!this.cellContent(cell) }; Grid.prototype.cellContent = function (cell) { if (this.withinBounds(cell)) { return this.cells[cell.x][cell.y] } else { return null } }; Grid.prototype.insertTile = function (tile) { this.cells[tile.x][tile.y] = tile }; Grid.prototype.removeTile = function (tile) { this.cells[tile.x][tile.y] = null }; Grid.prototype.withinBounds = function (position) { return (position.x >= 0 && position.x < this.size && position.y >= 0 && position.y < this.size) }; function HTMLActuator() { this.tileContainer = document.getElementsByClassName('tile-container')[0]; this.scoreContainer = document.getElementsByClassName('score-container')[0]; this.messageContainer = document.getElementsByClassName('game-message')[0]; this.score = 0 } HTMLActuator.prototype.actuate = function (grid, metadata) { var self = this; window.requestAnimationFrame(function () { self.clearContainer(self.tileContainer); grid.cells.forEach(function (column) { column.forEach(function (cell) { if (cell) { self.addTile(cell) } }) }); self.updateScore(metadata.score); if (metadata.over) self.message(false); if (metadata.won) self.message(true) }) }; HTMLActuator.prototype.restart = function () { this.clearMessage() }; HTMLActuator.prototype.clearContainer = function (container) { while (container.firstChild) { container.removeChild(container.firstChild) } }; HTMLActuator.prototype.addTile = function (tile) { var self = this; var element = document.createElement('div'); var position = tile.previousPosition || { x: tile.x, y: tile.y }; positionClass = this.positionClass(position); var classes = ['tile', 'tile-' + tile.value, positionClass]; this.applyClasses(element, classes); element.textContent = tile.value; if (tile.previousPosition) { window.requestAnimationFrame(function () { classes[2] = self.positionClass({ x: tile.x, y: tile.y }); self.applyClasses(element, classes) }) } else if (tile.mergedFrom) { classes.push('tile-merged'); this.applyClasses(element, classes); tile.mergedFrom.forEach(function (merged) { self.addTile(merged) }) } else { classes.push('tile-new'); this.applyClasses(element, classes) } this.tileContainer.appendChild(element) }; HTMLActuator.prototype.applyClasses = function (element, classes) { element.setAttribute('class', classes.join(' ')) }; HTMLActuator.prototype.normalizePosition = function (position) { return { x: position.x + 1, y: position.y + 1 } }; HTMLActuator.prototype.positionClass = function (position) { position = this.normalizePosition(position); return 'tile-position-' + position.x + '-' + position.y }; HTMLActuator.prototype.updateScore = function (score) { this.clearContainer(this.scoreContainer); var difference = score - this.score; this.score = score; this.scoreContainer.textContent = this.score; if (difference > 0) { var addition = document.createElement('div'); addition.classList.add('score-addition'); addition.textContent = '+' + difference; this.scoreContainer.appendChild(addition) } }; HTMLActuator.prototype.message = function (won) { var type = won ? 'game-won' : 'game-over'; var message = won ? 'You win!' : 'Game over!'; this.messageContainer.classList.add(type); this.messageContainer.getElementsByTagName('p')[0].textContent = message }; HTMLActuator.prototype.clearMessage = function () { this.messageContainer.classList.remove('game-won', 'game-over') }; function KeyboardInputManager() { this.events = {}; this.listen() } KeyboardInputManager.prototype.on = function (event, callback) { if (!this.events[event]) { this.events[event] = [] } this.events[event].push(callback) }; KeyboardInputManager.prototype.emit = function (event, data) { var callbacks = this.events[event]; if (callbacks) { callbacks.forEach(function (callback) { callback(data) }) } }; KeyboardInputManager.prototype.listen = function () { var self = this; var map = { 38: 0, 39: 1, 40: 2, 37: 3, 75: 0, 76: 1, 74: 2, 72: 3, }; document.addEventListener('keydown', function (event) { var modifiers = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; var mapped = map[event.which]; if (!modifiers) { if (mapped !== undefined) { event.preventDefault(); self.emit('move', mapped) } if (event.which === 32) self.restart.bind(self)(event) } }); var retry = document.getElementsByClassName('retry-button')[0]; retry.addEventListener('click', this.restart.bind(this)); var gestures = [Hammer.DIRECTION_UP, Hammer.DIRECTION_RIGHT, Hammer.DIRECTION_DOWN, Hammer.DIRECTION_LEFT,]; var gameContainer = document.getElementsByClassName('game-container')[0]; var handler = Hammer(gameContainer, { drag_block_horizontal: true, drag_block_vertical: true, }); handler.on('swipe', function (event) { event.gesture.preventDefault(); mapped = gestures.indexOf(event.gesture.direction); if (mapped !== -1) self.emit('move', mapped) }) }; KeyboardInputManager.prototype.restart = function (event) { event.preventDefault(); this.emit('restart') }; function Tile(position, value) { this.x = position.x; this.y = position.y; this.value = value || 2; this.previousPosition = null; this.mergedFrom = null } Tile.prototype.savePosition = function () { this.previousPosition = { x: this.x, y: this.y } }; Tile.prototype.updatePosition = function (position) { this.x = position.x; this.y = position.y };</script>
</body>

</html>